/*
 * ContentEnumerator.cs
 * 
 * Microsoft Office SharePoint Server Managed Protocol Handler
 * 
 * Author: John Kozell (johnkoz@microsoft.com)
 *         Microsoft Coroporation
 * 
 * This code is an adaptation from, and interpretation of, the original SPS 2001 SDK
 * and has been updated with available information at the time of writing.
 * Not all the delcarions and alogrithms have been verified and tested
 * and there is no guarantee that they are accurate or valid, now or in the future.
 * 
 * The .NET Managed COM Interop approach contained herin was NOT created in cooperation
 * with the Microsoft SharePoint product group and has not been officially endorced
 * by Microsoft and therefore is not supported. The user of this code assumes any
 * and all risk with its use and should proceed with discression.
 * 
 * THIS CODE AND INFORMATION ARE PROVIDED AS IS WITHOUT WARRANTY OF ANY
 * KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
 * PARTICULAR PURPOSE.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Web;
using System.Runtime.InteropServices;
using System.Xml;

namespace MOSSPH
{
    /// <summary>
    /// This class abstracts access to the protocol handlers underlying content, as addressed by a content source URL.
    /// It is capable of enumerating containers OR items/rows. 
    /// </summary>
    public class ContentEnumerator : IContentEnumerator
    {
        public const string ProtocolPrefix = "MOSSPH"; // TODO. Change to your custom protocol acronym.
        private static Guid PROPERTY_CATEGORY = new Guid("F7C8FFBD-8A3A-4df0-A7CF-A384745A6EA4"); // TODO. Create a new GUID if you will be returning custom properties.

        #region "XML Sample code. TODO. Replace with your implementation"
        private XmlDocument m_Content = new XmlDocument(); // Holds all the content for this sample, any container or document.
        private XmlNode m_ThisContent = null; // A reference to the xml node holding the container or document.
        private string m_ItemFilePath = string.Empty; // When a temporary local file is made the full path is placed here.
        private List<CrawlUrl> m_Containers = new List<CrawlUrl>(); // Holds the URL's of the containers within this container.
        private List<CrawlUrl> m_Items = new List<CrawlUrl>(); // Holds the URL's of the items within this container.
        #endregion
        
        // Enumerators only used if the item is a container.
        private IEnumerator m_ContainerEnumerator = null; // Itterates over the container in this container.
        private IEnumerator m_ItemEnumerator = null; // Itterates over the items in this container.

        private string m_UserAgent = string.Empty; // Set during Init(), from the PROXY_INFO structure.
        private Uri m_Url = null; // Set in Init().
        private bool m_bIsContainer = false; // Assume an item, updated once the URL is parsed in Init().
        private byte[] m_SecurityDescriptor = null;
        private Stream m_Stream = null; // Holds the contents of the downloaded page.
        private ulong m_Size = 0; // How big the downloaded page is.
        private string m_ContentType = null;
        private ArrayList m_Properties = new ArrayList(); // Additional custom properties for each item or container, array of DictionaryEntry items.


        /// <summary>
        /// Simple instantiation, the URL is initialized in the Init() method.
        /// </summary>
        public ContentEnumerator()
        {
            Logging.Information("Creating ContentEnumerator");
        }

         /// <summary>
        /// This method will initialize the enumeration constructs based on the "Type" of the URL.
        /// </summary>
        /// <returns></returns>
        public HRESULT Init(string sUrl, string sUserAgent)
        {
            DateTime enterTime = Logging.Enter(typeof(ContentEnumerator), "Init", sUrl);
            HRESULT hr = HRESULT.E_FAIL; // Assume we fail until we get through all the initialization.
            string sOperation = "Open Content Source"; // Alternative to multiple try/catch's.

            try
            {
                sOperation = "Store URL";
                m_Url = new Uri(sUrl);

                // Parse URL to determine if this is a container or item.
                sOperation = "Parsing URL";

                #region "XML Sample code. TODO. Replace with your implementation"
                string[] tokens = Url.Split('/');
                if (tokens.Length >= 3)
                {
                    string fileName = AppDomain.CurrentDomain.BaseDirectory + tokens[2] + ".xml";
                    Logging.Trace("Opening XML file: " + fileName);
                    m_Content.Load(fileName);

                    XmlNode rootContainer = m_Content.SelectSingleNode("CONTAINER");
                    XmlNode thisContainer = rootContainer;

                    m_bIsContainer = true; // Set to 'true' if m_Url is a container/directory, otherwise 'false'.
                    if (tokens.Length == 3 || (tokens.Length == 4 && string.IsNullOrEmpty(tokens[tokens.Length - 1])))
                    {
                        m_Containers.Add(new CrawlUrl(Url + rootContainer.Attributes["Name"].Value, GetLastModifiedFromXML(rootContainer)));
                        m_ThisContent = rootContainer;
                    }
                    else
                    {
                        for (int ti = 4; ti < tokens.Length; ti++)
                        {
                            string token = tokens[ti];
                            if (!string.IsNullOrEmpty(token))
                            {
                                if (token.IndexOf('.') < 0)
                                {
                                    string xpath = string.Format("CONTAINERS/CONTAINER[@Name='{0}']", token);
                                    Logging.Trace(string.Format("Looking for {0} in {1}", xpath, thisContainer.Attributes["Name"].Value));
                                    XmlNode container = thisContainer.SelectSingleNode(xpath);
                                    if (container != null)
                                        thisContainer = container;
                                }
                                else
                                {
                                    string xpath = string.Format("ITEMS/ITEM[@Name='{0}']", token);
                                    Logging.Trace(string.Format("Looking for {0} in {1}", xpath, thisContainer.Attributes["Name"].Value));
                                    m_ThisContent = thisContainer.SelectSingleNode(xpath);

                                    m_bIsContainer = false; // end of URL contains a dot (.), so it is an item.
                                    break;
                                }
                            }
                            else // Trailing slash is the same as no token, take the previous container.
                                break;
                        }

                        // Initialize the enumerators.
                        sOperation = "Initializing enumerators";
                        Logging.Trace("Current XML Container: " + thisContainer.Attributes["Name"].Value);
                        if (IsContainer)
                        {
                            XmlNodeList containers = thisContainer.SelectNodes("CONTAINERS/CONTAINER");
                            if (containers != null)
                            {
                                foreach (XmlNode container in containers)
                                {
                                    string url = Url + "/" + container.Attributes["Name"].Value;
                                    DateTime lastModified = GetLastModifiedFromXML(container);
                                    Logging.Trace(string.Format("Adding container to enumeration array: {0} ({1})", url, lastModified));
                                    m_Containers.Add(new CrawlUrl(url, lastModified));
                                }
                            }

                            XmlNodeList items = thisContainer.SelectNodes("ITEMS/ITEM");
                            if (items != null)
                            {
                                foreach (XmlNode item in items)
                                {
                                    string url = Url + "/" + item.Attributes["Name"].Value;
                                    DateTime lastModified = GetLastModifiedFromXML(item);
                                    Logging.Trace(string.Format("Adding item to enumeration array: {0} ({1})", url, lastModified));
                                    m_Items.Add(new CrawlUrl(url, lastModified));
                                }
                            }

                            m_ThisContent = thisContainer;
                        }
                    }
                    if (m_Containers.Count > 0)
                        m_ContainerEnumerator = m_Containers.GetEnumerator();
                    if (m_Items.Count > 0)
                        m_ItemEnumerator = m_Items.GetEnumerator();

                    hr = (HRESULT)SCODE.S_OK; // Success.
                }
                #endregion // Make sure 'hr' is set correctly (S_OK) after removing this region.
            }
            catch (Exception e)
            {
                // Use sOperation to log what step the exception was thrown.
                Logging.Exception(sOperation, e);
                hr = HRESULT.E_FAIL;
            }

            Logging.Leave(typeof(ContentEnumerator), "Init", null, enterTime);
            return hr;
        }

        #region "XML Sample code. TODO. Replace with your implementation"
        private DateTime GetLastModifiedFromXML(XmlNode node)
        {
            DateTime lastModified = DateTime.Parse("1/1/2000"); // The more correct default would be DateTime.Now, but this aid in testing.
            XmlAttribute lastModifiedAttribute = node.Attributes["LastModified"];
            if (lastModifiedAttribute != null)
                lastModified = DateTime.Parse(lastModifiedAttribute.Value);
            return lastModified;
        }
        #endregion

        public void Terminate()
        {
            m_bIsContainer = false;
            m_SecurityDescriptor = null;
            m_ItemEnumerator = null;
            m_ContainerEnumerator = null;

            Logging.Information("ContentEnumerator terminated");
        }


        public void EnsureContent()
        {
            DateTime enterTime = Logging.Enter(typeof(ContentEnumerator), "EnsureContent");

            // TODO. Add code to perform the actual download/extraction of content for the item.
            // 1. Place content in the m_Stream member for later read through the IStream interface.
            // 2. Fetch the size of the current item and place it in m_Size.
            // 3. Fetch the content type of current item and place it in m_ContentType.
            // Note: Use the "Download" function (provided below) if your content can be fetched via HTTP.
            //       Construct a suitable URL prior to calling Download.

            #region "XML Sample code. TODO. Replace with your implementation"
            XmlNode properties = m_ThisContent.SelectSingleNode("PROPERTIES");
            if (properties != null)
            {
                foreach (XmlNode property in properties.ChildNodes)
                {
                    string name = property.Attributes["Name"].Value;
                    string val = property.InnerText;
                    Logging.Trace(string.Format("Found property {0}: {1}", name, val));
                    m_Properties.Add(new Property(PROPERTY_CATEGORY, name, val));
                }
            }
            
            XmlNode content = m_ThisContent.SelectSingleNode("CONTENT");
            if (content != null)
            {
                m_ItemFilePath = GetTempFoder() + m_ThisContent.Attributes["Name"].Value;
                Logging.Trace("Writing content to: " + m_ItemFilePath);

                StreamWriter file = new StreamWriter(m_ItemFilePath);
                string contentText = content.InnerText;
                m_Size = (ulong)contentText.Length;
                file.Write(contentText);
                file.Close();
            }
            #endregion

            Logging.Leave(typeof(ContentEnumerator), "EnsureContent", string.Empty, enterTime);
        }

        /// <summary>
        /// This function is used instead of the System.Path.GetTempPath() function becaues that yields a sibbling directory
        /// to the gthrsvc directory and not the parent (temp). So we hard-code the temp directory here, since this is just a sample.
        /// </summary>
        /// <returns>
        /// The gatherer's temporary working directory. This directory is cleaned-up by the gatherer upon starting/stopping.
        /// </returns>
        private string GetTempFoder()
        {
            return string.Format(@"C:\Documents and Settings\{0}\Local Settings\Temp\gthrsvc\", Environment.UserName);
        }


        #region Properties

        /// <summary>
        /// Returns the URL that this content enumerator was initialized.
        /// </summary>
        public string Url
        {
            get
            {
                string url = string.Empty;
                if (m_Url != null)
                    url = m_Url.ToString();
                return url;
            }
        }


        /// <summary>
        /// Fetch the DateTime of the last modification to the container or item.
        /// An invalid type will return a MinValue for the DateTime.
        /// </summary>
        public DateTime LastModified
        {
            get
            {
                DateTime lastmodified = Convert.ToDateTime("01.01.1901");

                #region "XML Sample code. TODO. Replace with your implementation"
                lastmodified = GetLastModifiedFromXML(m_ThisContent);
                #endregion

                return lastmodified;
            }
        }


        /// <summary>
        /// Return the size of an item. Containers always have a size of zero (0).
        /// </summary>
        public ulong Size
        {
            get
            {
                ulong size = 0;
                if (!IsContainer)
                    size = m_Size;
                return size;
            }
        }


        /// <summary>
        /// If this is an Item and it exists in a file, reutrn the full path to the file here.
        /// Otherwise return the IStream in the "Content" property and return a 'null' here.
        /// </summary>
        public string FilePath
        {
            get
            {
                #region "XML Sample code. TODO. Replace with your implementation"
                return m_ItemFilePath;
                #endregion
            }
        }
        

        /// <summary>
        /// Provides an IStream interface to read the item content.
        /// Implement this if you are not using a disk-based file and the "FilePath" property.
        /// If using this property this object implements IStream, so we just return a reference to ourself.
        /// </summary>
        public System.Runtime.InteropServices.ComTypes.IStream Content
        {
            get
            {
                return null; // TODO. Return 'this' if not using the "FilePath" property and using a memory stream instead.
            }
        }


        /// <summary>
        /// Content type is interesting only for items.
        /// </summary>
        public string ContentType
        {
            get
            {
                string contenttype = null;
                if (!IsContainer)
                    contenttype = m_ContentType;
                return contenttype;
            }
        }


        /// <summary>
        /// This is the anchor for the URL presented to the user during in their search results.
        /// The assumption is that it will be different than the Content Source URL privided to the indexer.
        /// </summary>
        public string LaunchUrl
        {
            get
            {
                string url = Url;

                // TODO. If using a different URL to link in the search results, place that assignment here.

                return url;
            }
        }


        /// <summary>
        /// The human readable name for the container or item.
        /// </summary>
        public string Title
        {
            get
            {
                string title = null;

                #region "XML Sample code. TODO. Replace with your implementation"
                title = m_ThisContent.Attributes["Name"].Value;
                #endregion

                return title;
            }
        }


        /// <summary>
        /// True if the URL provided points to a content continer, otherwise False.
        /// </summary>
        public bool IsContainer
        {
            get
            {
                return m_bIsContainer;
            }
        }


        
        /// <summary>
        /// Returns a relative Win32 security descriptor to the current Container or Item, as a byte array.
        /// </summary>
        public byte[] SecurityDescriptor
        {
            get
            {
                if (m_SecurityDescriptor == null)
                {
                    DateTime startTime = Logging.Enter(typeof(ContentEnumerator), "SecurityDescriptor-create");

                    #region "XML Sample code. TODO. Replace with your implementation"
                    List<string> users = new List<string>();

                    XmlNode security = m_ThisContent.SelectSingleNode("SECURITY");
                    if (security!= null)
                    {
                        foreach (XmlNode ace in security.ChildNodes)
                        {
                            string name = ace.Attributes["Name"].Value;
                            users.Add(name);
                        }
                    }
                    if (users.Count > 0)
                    {
                        PHSecurityDescriptor sd = new PHSecurityDescriptor();
                        sd.LoadSecurity(Environment.UserName, users.ToArray());
                        m_SecurityDescriptor = sd.ToBytes();
                    }
                    #endregion

                    Logging.Leave(typeof(ContentEnumerator), "SecurityDescriptor-create", "Length: " + (m_SecurityDescriptor != null ? m_SecurityDescriptor.Length.ToString() : "(null)"), startTime);
                }

                return m_SecurityDescriptor;
            }
        }


        /// <summary>
        /// 
        /// </summary>
        public ArrayList Properties
        {
            get
            {
                return m_Properties;
            }
        }


        #endregion // Properties

        #region Container enumeration

        // TODO. Modify the functions in this region so that the functions perform an itteration through the URLs
        // of sub-containers of the current container (if any). It is possible to have an IEnumerator that
        // returns an object for which you can derive a URL.

        /// <summary>
        /// Rewindes the enumeration back to the first container.
        /// </summary>
        public void ResetContainer()
        {
            if (m_ContainerEnumerator != null)
                m_ContainerEnumerator.Reset();
        }


        /// <summary>
        /// Returns the URL of the current sub-container.
        /// </summary>
        public CrawlUrl CurrentContainer
        {
            get
            {
                CrawlUrl currentContainer = new CrawlUrl();

                if (m_ContainerEnumerator != null)
                {
                    try
                    {
                        currentContainer = (CrawlUrl)m_ContainerEnumerator.Current;
                    }
                    catch (Exception e)
                    {
                        Logging.Exception("Cannot construct Url for Container", e);
                    };
                }
                return currentContainer;
            }
        }

        /// <summary>
        /// Advances to the next sub-container and returns its URL.
        /// </summary>
        /// <returns></returns>
        public CrawlUrl GetNextContainer()
        {
            CrawlUrl cuReturn = new CrawlUrl();

            if (m_ContainerEnumerator != null)
            {
                // Loop past any exceptions.
                while (m_ContainerEnumerator.MoveNext())
                {
                    if (true) // TODO. Insert any validation code required in case you want to skip past some containers.
                    {
                        cuReturn = CurrentContainer;
                        break;
                    }
                }
            }
            return cuReturn;
        }

        #endregion // Container enumeration

        #region Item enumeration

        // TODO. Modify the functions in this region so that the functions perform an itteration through the URLs
        // of items of the current container (if any). It is possible to have an IEnumerator that
        // returns an object for which you can derive a URL.

        /// <summary>
        /// Rewindes the enumeration back to the first item.
        /// </summary>
        public void ResetItems()
        {
            if (m_ItemEnumerator != null)
                m_ItemEnumerator.Reset();
        }

        /// <summary>
        /// Returns the URL of the current item.
        /// </summary>
        public CrawlUrl CurrentItem
        {
            get
            {
                CrawlUrl currentItem = new CrawlUrl();

                if (m_ItemEnumerator != null)
                {
                    try
                    {
                        currentItem = (CrawlUrl)m_ItemEnumerator.Current;
                    }
                    catch
                    {
                    }
                }
                return currentItem;
            }
        }

        /// <summary>
        /// Advances to the next item and returns its URL.
        /// </summary>
        /// <returns></returns>
        public CrawlUrl GetNextItem()
        {
            CrawlUrl cuReturn = new CrawlUrl();

            if (m_ItemEnumerator != null)
            {
                if (m_ItemEnumerator.MoveNext())
                {
                    cuReturn = CurrentItem;
                }
            }

            return cuReturn;
        }

        #endregion // Item enumeration

        #region IStream members

        void System.Runtime.InteropServices.ComTypes.IStream.Commit(int grfCommitFlags)
        {
            Logging.Called(typeof(ContentEnumerator), "Commit");
        }

        void System.Runtime.InteropServices.ComTypes.IStream.Clone(out System.Runtime.InteropServices.ComTypes.IStream ppstm)
        {
            Logging.Called(typeof(ContentEnumerator), "Clone");
            ppstm = null;
        }

        void System.Runtime.InteropServices.ComTypes.IStream.LockRegion(long libOffset, long cb, int dwLockType)
        {
            Logging.Called(typeof(ContentEnumerator), "LockRegion");
        }


        /// <summary>
        /// This is the only method the filter daemon calls from the IStream interface.
        /// The read method will manage the extraction of the content from the m_Stream member
        /// previously fetched during EnsureContent.
        /// </summary>
        /// <param name="pv"></param>
        /// <param name="cb"></param>
        /// <param name="pcbRead"></param>
        public void Read(byte[] pv, int cb, System.IntPtr pcbRead)
        {
            Logging.Enter(typeof(ContentEnumerator), "Read");
            HRESULT hr = (HRESULT)SCODE.S_FALSE;
            string sOperation = null;

            try
            {
                sOperation = "Read";
                int iRead = m_Stream.Read(pv, 0, (int)cb);

                if (iRead > 0)
                {
                    // Log some text.
                    sOperation = "Log sample of byte recieved";
                    char[] bufferSample = new char[200];
                    for (int i = 0; i < bufferSample.Length; i++)
                        bufferSample[i] = (char)pv[i];
                    Logging.Trace("Read " + iRead.ToString() + " bytes: " + new string(bufferSample));
                    hr = 0;
                }

                sOperation = "Marshal #bytes read back to caller";
                Marshal.WriteInt32(pcbRead, iRead);
            }
            catch (Exception e)
            {
                Logging.Exception(sOperation, e);
                hr = (HRESULT)SCODE.S_FALSE;
            }

            Logging.Leave(typeof(ContentEnumerator), "Read");
            if (hr != (HRESULT)SCODE.S_OK)
            {
                Marshal.ThrowExceptionForHR((int)hr);
            }
        }

        public void Revert()
        {
            Logging.Called(typeof(ContentEnumerator), "Revert");
        }

        public void CopyTo(System.Runtime.InteropServices.ComTypes.IStream pstm, long cb, System.IntPtr pcbRead, System.IntPtr pcbWritten)
        {
            Logging.Called(typeof(ContentEnumerator), "CopyTo");
        }

        public void Seek(long dlibMove, int dwOrigin, System.IntPtr plibNewPosition)
        {
            Logging.Called(typeof(ContentEnumerator), "Seek");
        }

        void System.Runtime.InteropServices.ComTypes.IStream.UnlockRegion(long libOffset, long cb, int dwLockType)
        {
            Logging.Called(typeof(ContentEnumerator), "UnlockRegion");
        }

        void System.Runtime.InteropServices.ComTypes.IStream.SetSize(long libNewSize)
        {
            Logging.Called(typeof(ContentEnumerator), "SetSize");
        }

        void System.Runtime.InteropServices.ComTypes.IStream.Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag)
        {
            Logging.Called(typeof(ContentEnumerator), "Stat");
            pstatstg = new System.Runtime.InteropServices.ComTypes.STATSTG();
        }

        public void Write(byte[] pv, int cb, System.IntPtr pcbWritten)
        {
            Logging.Called(typeof(ContentEnumerator), "Write");
        }

        #endregion // IStream members

        #region Optional components

        /// <summary>
        /// Uses httpwebrequest and httpwebresponse to download the HTML at sUrl and save it in m_Stream. 
        /// </summary>
        /// <param name="sUrl">The URL of the page to be downloaded</param>
        /// <returns></returns>
        private bool Download(string sUrl)
        {
            DateTime enterTime = Logging.Enter(typeof(ContentEnumerator), "Download");
            bool bSuccess = false;
            string sOperation = null;

            Logging.Information("Downloading page: " + sUrl);

            try
            {
                // prepare the web page we will be asking for
                sOperation = "WebRequest.Create";
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(sUrl);
                request.UserAgent = m_UserAgent;
                request.Credentials = CredentialCache.DefaultCredentials; // TODO. Establish your own security context.

                // execute the request
                sOperation = "request.GetResponse";
                HttpWebResponse response = (HttpWebResponse)request.GetResponse();

                // we will read data via the response stream
                m_ContentType = response.ContentType;
                m_Size = (ulong)response.ContentLength;
                sOperation = "response.GetResponseStream";
                m_Stream = response.GetResponseStream();
                Logging.Information("Content Type: " + m_ContentType + ", Size: " + m_Size.ToString());
                bSuccess = true;
            }
            catch (Exception ex)
            {
                Logging.Exception(sOperation, ex);
            }
            Logging.Leave(typeof(ContentEnumerator), "Download", bSuccess.ToString(), enterTime);
            return bSuccess;
        }

        #endregion // Optional components.
    }
}
